package panda.io;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import panda.lang.Collections;


/**
 * This class implements an output stream in which the data is written into a byte array. The buffer
 * automatically grows as data is written to it.
 * <p>
 * The data can be retrieved using <code>toByteArray()</code> and <code>toString()</code>.
 * <p>
 * Closing a <tt>ByteArrayOutputStream</tt> has no effect. The methods in this class can be called
 * after the stream has been closed without generating an <tt>IOException</tt>.
 * <p>
 * This is an alternative implementation of the {@link java.io.ByteArrayOutputStream} class. The
 * original implementation only allocates 32 bytes at the beginning. As this class is designed for
 * heavy duty it starts at 1024 bytes. In contrast to the original it doesn't reallocate the whole
 * memory block but allocates additional buffers. This way no buffers need to be garbage collected
 * and the contents don't have to be copied to the new buffer. This class is designed to behave
 * exactly like the original. The only exception is the deprecated toString(int) method that has
 * been ignored.
 * 
 * @author yf.frank.wang@gmail.com
 */
public class ByteArrayOutputStream extends OutputStream {

	/** A singleton empty byte array. */
	private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];

	/** The list of buffers, which grows and never reduces. */
	private final List<byte[]> buffers = new ArrayList<byte[]>();
	/** The index of the current buffer. */
	private int currentBufferIndex;
	/** The total count of bytes in all the filled buffers. */
	private int filledBufferSum;
	/** The current buffer. */
	private byte[] currentBuffer;
	/** The total count of bytes written. */
	private int count;

	/**
	 * Creates a new byte array output stream. The buffer capacity is initially 1024 bytes, though
	 * its size increases if necessary.
	 */
	public ByteArrayOutputStream() {
		this(1024);
	}

	/**
	 * Creates a new byte array output stream, with a buffer capacity of the specified size, in
	 * bytes.
	 * 
	 * @param size the initial size
	 * @throws IllegalArgumentException if size is negative
	 */
	public ByteArrayOutputStream(int size) {
		if (size < 0) {
			throw new IllegalArgumentException("Negative initial size: " + size);
		}
		synchronized (this) {
			needNewBuffer(size);
		}
	}

	/**
	 * Makes a new buffer available either by allocating a new one or re-cycling an existing one.
	 * 
	 * @param newcount the size of the buffer if one is created
	 */
	private void needNewBuffer(int newcount) {
		if (currentBufferIndex < buffers.size() - 1) {
			// Recycling old buffer
			filledBufferSum += currentBuffer.length;

			currentBufferIndex++;
			currentBuffer = buffers.get(currentBufferIndex);
		}
		else {
			// Creating new buffer
			int newBufferSize;
			if (currentBuffer == null) {
				newBufferSize = newcount;
				filledBufferSum = 0;
			}
			else {
				newBufferSize = Math.max(currentBuffer.length << 1, newcount - filledBufferSum);
				filledBufferSum += currentBuffer.length;
			}

			currentBufferIndex++;
			currentBuffer = new byte[newBufferSize];
			buffers.add(currentBuffer);
		}
	}

	/**
	 * Write the bytes to byte array.
	 * 
	 * @param b the bytes to write
	 * @param off The start offset
	 * @param len The number of bytes to write
	 */
	@Override
	public void write(byte[] b, int off, int len) {
		if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length)
				|| ((off + len) < 0)) {
			throw new IndexOutOfBoundsException();
		}
		else if (len == 0) {
			return;
		}
		synchronized (this) {
			int newcount = count + len;
			int remaining = len;
			int inBufferPos = count - filledBufferSum;
			while (remaining > 0) {
				int part = Math.min(remaining, currentBuffer.length - inBufferPos);
				System.arraycopy(b, off + len - remaining, currentBuffer, inBufferPos, part);
				remaining -= part;
				if (remaining > 0) {
					needNewBuffer(newcount);
					inBufferPos = 0;
				}
			}
			count = newcount;
		}
	}

	/**
	 * Write a byte to byte array.
	 * 
	 * @param b the byte to write
	 */
	@Override
	public synchronized void write(int b) {
		int inBufferPos = count - filledBufferSum;
		if (inBufferPos == currentBuffer.length) {
			needNewBuffer(count + 1);
			inBufferPos = 0;
		}
		currentBuffer[inBufferPos] = (byte)b;
		count++;
	}

	/**
	 * Writes the entire contents of the specified input stream to this byte stream. Bytes from the
	 * input stream are read directly into the internal buffers of this streams.
	 * 
	 * @param in the input stream to read from
	 * @return total number of bytes read from the input stream (and written to this stream)
	 * @throws IOException if an I/O error occurs while reading the input stream
	 * @since 1.4
	 */
	public synchronized int write(InputStream in) throws IOException {
		int readCount = 0;
		int inBufferPos = count - filledBufferSum;
		int n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos);
		while (n != -1) {
			readCount += n;
			inBufferPos += n;
			count += n;
			if (inBufferPos == currentBuffer.length) {
				needNewBuffer(currentBuffer.length);
				inBufferPos = 0;
			}
			n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos);
		}
		return readCount;
	}

	/**
	 * Return the current size of the byte array.
	 * 
	 * @return the current size of the byte array
	 */
	public synchronized int size() {
		return count;
	}

	/**
	 * Closing a <tt>ByteArrayOutputStream</tt> has no effect. The methods in this class can be
	 * called after the stream has been closed without generating an <tt>IOException</tt>.
	 * 
	 * @throws IOException never (this method should not declare this exception but it has to now
	 *             due to backwards compatability)
	 */
	@Override
	public void close() throws IOException {
		// nop
	}

	/**
	 * @see java.io.ByteArrayOutputStream#reset()
	 */
	public synchronized void reset() {
		count = 0;
		filledBufferSum = 0;
		currentBufferIndex = 0;
		currentBuffer = buffers.get(currentBufferIndex);
	}

	/**
	 * Writes the entire contents of this byte stream to the specified output stream.
	 * 
	 * @param out the output stream to write to
	 * @throws IOException if an I/O error occurs, such as if the stream is closed
	 * @see java.io.ByteArrayOutputStream#writeTo(OutputStream)
	 */
	public synchronized void writeTo(OutputStream out) throws IOException {
		int remaining = count;
		for (byte[] buf : buffers) {
			int c = Math.min(buf.length, remaining);
			out.write(buf, 0, c);
			remaining -= c;
			if (remaining == 0) {
				break;
			}
		}
	}

	/**
	 * Fetches entire contents of an <code>InputStream</code> and represent same data as result
	 * InputStream.
	 * <p>
	 * This method is useful where,
	 * <ul>
	 * <li>Source InputStream is slow.</li>
	 * <li>It has network resources associated, so we cannot keep it open for long time.</li>
	 * <li>It has network timeout associated.</li>
	 * </ul>
	 * It can be used in favor of {@link #toByteArray()}, since it avoids unnecessary allocation and
	 * copy of byte[].<br>
	 * This method buffers the input internally, so there is no need to use a
	 * <code>BufferedInputStream</code>.
	 * 
	 * @param input Stream to be fully buffered.
	 * @return A fully buffered stream.
	 * @throws IOException if an I/O error occurs
	 */
	public static InputStream toBufferedInputStream(InputStream input) throws IOException {
		ByteArrayOutputStream output = new ByteArrayOutputStream();
		try {
			output.write(input);
			return output.toBufferedInputStream();
		}
		finally {
			output.close();
		}
	}

	/**
	 * Gets the current contents of this byte stream as a Input Stream. The returned stream is
	 * backed by buffers of <code>this</code> stream, avoiding memory allocation and copy, thus
	 * saving space and time.<br>
	 * 
	 * @return the current contents of this output stream.
	 * @see java.io.ByteArrayOutputStream#toByteArray()
	 * @see #reset()
	 */
	private InputStream toBufferedInputStream() {
		int remaining = count;
		if (remaining == 0) {
			return Streams.emptyInputStream();
		}
		List<ByteArrayInputStream> list = new ArrayList<ByteArrayInputStream>(buffers.size());
		for (byte[] buf : buffers) {
			int c = Math.min(buf.length, remaining);
			list.add(new ByteArrayInputStream(buf, 0, c));
			remaining -= c;
			if (remaining == 0) {
				break;
			}
		}
		return new SequenceInputStream(Collections.enumeration(list));
	}

	/**
	 * Gets the curent contents of this byte stream as a byte array. The result is independent of
	 * this stream.
	 * 
	 * @return the current contents of this output stream, as a byte array
	 * @see java.io.ByteArrayOutputStream#toByteArray()
	 */
	public synchronized byte[] toByteArray() {
		int remaining = count;
		if (remaining == 0) {
			return EMPTY_BYTE_ARRAY;
		}
		byte newbuf[] = new byte[remaining];
		int pos = 0;
		for (byte[] buf : buffers) {
			int c = Math.min(buf.length, remaining);
			System.arraycopy(buf, 0, newbuf, pos, c);
			pos += c;
			remaining -= c;
			if (remaining == 0) {
				break;
			}
		}
		return newbuf;
	}

	/**
	 * Gets the curent contents of this byte stream as a string.
	 * 
	 * @return the contents of the byte array as a String
	 * @see java.io.ByteArrayOutputStream#toString()
	 */
	@Override
	public String toString() {
		return new String(toByteArray());
	}

	/**
	 * Gets the curent contents of this byte stream as a string using the specified encoding.
	 * 
	 * @param enc the name of the character encoding
	 * @return the string converted from the byte array
	 * @throws UnsupportedEncodingException if the encoding is not supported
	 * @see java.io.ByteArrayOutputStream#toString(String)
	 */
	public String toString(String enc) throws UnsupportedEncodingException {
		return new String(toByteArray(), enc);
	}

}
